package s3

import (
	"context"
	"io"
	"net/http"
	"time"

	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/aws/awserr"
	"github.com/aws/aws-sdk-go/service/s3"
	"github.com/aws/aws-sdk-go/service/s3/s3manager"
)

// S3Template 定义s3防腐层，对外不需要感知s3 sdk的存在
type S3Template interface {
	MultiPartUpload(ctx context.Context, bucket string, key string, body io.ReadCloser) error
	PutObject(ctx context.Context, bucket string, key string, body io.ReadSeeker) error
	DeleteObject(ctx context.Context, bucket string, key string) error
	ObjectExists(ctx context.Context, bucket string, key string) (bool, error)
	GetObject(ctx context.Context, bucket string, key string) (*S3ObjectWrapper, error)
	PresignDownloadURL(ctx context.Context, bucket string, key string, expired time.Duration) (string, error)

	ListBuckets(ctx context.Context) ([]string, error)
	CreateBucket(ctx context.Context, bucket string) error
	BucketExists(ctx context.Context, bucket string) (bool, error)
}

const (
	DefaultRegion = "default"
)

type S3ObjectWrapper struct {
	*s3.GetObjectOutput
}

func (s3obj *S3ObjectWrapper) Close() {
	s3obj.Body.Close()
}

func (s3obj *S3ObjectWrapper) GetBody() io.ReadCloser {
	return s3obj.Body
}

func (s3obj *S3ObjectWrapper) GetEtag() string {
	return aws.StringValue(s3obj.ETag)
}

type SimpleS3Template struct {
	factory S3ClientFactory
}

func NewSimpleS3Template(factory S3ClientFactory) *SimpleS3Template {
	return &SimpleS3Template{
		factory: factory,
	}
}

var _ S3Template = (*SimpleS3Template)(nil)

func (st *SimpleS3Template) PutObject(ctx context.Context, bucket string, key string, body io.ReadSeeker) error {
	client, err := st.factory.GetS3RawClient()
	if err != nil {
		return err
	}
	_, err = client.PutObject(&s3.PutObjectInput{
		Bucket: aws.String(bucket),
		Key:    aws.String(key),
		Body:   body,
	})
	return err
}

func (st *SimpleS3Template) MultiPartUpload(ctx context.Context, bucket string, key string, body io.ReadCloser) error {
	return nil
}

func (st *SimpleS3Template) DeleteObject(ctx context.Context, bucket string, key string) error {
	client, err := st.factory.GetS3RawClient()
	if err != nil {
		return err
	}
	_, err = client.DeleteObject(&s3.DeleteObjectInput{
		Bucket: aws.String(bucket),
		Key:    aws.String(key),
	})
	return err
}

func (st *SimpleS3Template) ObjectExists(ctx context.Context, bucket string, key string) (bool, error) {
	client, err := st.factory.GetS3RawClient()
	if err != nil {
		return false, err
	}
	if _, err = client.HeadObject(&s3.HeadObjectInput{
		Bucket: aws.String(bucket),
		Key:    aws.String(key),
	}); err == nil {
		return true, nil
	}

	if requestErr, ok := err.(awserr.RequestFailure); ok {
		if requestErr.StatusCode() == http.StatusNotFound {
			return false, nil
		}
	}
	return false, err
}

func (st *SimpleS3Template) GetObject(ctx context.Context, bucket string, key string) (*S3ObjectWrapper, error) {
	client, err := st.factory.GetS3RawClient()
	if err != nil {
		return nil, err
	}
	out, err := client.GetObject(&s3.GetObjectInput{
		Bucket: aws.String(bucket),
		Key:    aws.String(key),
	})
	if err != nil {
		return nil, err
	}
	return &S3ObjectWrapper{out}, nil
}

func (st *SimpleS3Template) PresignDownloadURL(ctx context.Context, bucket string, key string, expire time.Duration) (string, error) {
	client, err := st.factory.GetS3RawClient()
	if err != nil {
		return "", err
	}
	request, _ := client.GetObjectRequest(&s3.GetObjectInput{
		Bucket: aws.String(bucket),
		Key:    aws.String(key),
	})
	return request.Presign(expire)
}

func (st *SimpleS3Template) ListBuckets(ctx context.Context) ([]string, error) {
	client, err := st.factory.GetS3RawClient()
	if err != nil {
		return nil, err
	}
	out, err := client.ListBuckets(&s3.ListBucketsInput{})
	if err != nil {
		return nil, err
	}
	var re = make([]string, 0, len(out.Buckets))
	for _, bucket := range out.Buckets {
		re = append(re, *bucket.Name)
	}
	return re, nil
}

func (st *SimpleS3Template) CreateBucket(ctx context.Context, bucket string) error {
	client, err := st.factory.GetS3RawClient()
	if err != nil {
		return err
	}
	_, err = client.CreateBucket(&s3.CreateBucketInput{
		Bucket: aws.String(bucket),
		CreateBucketConfiguration: &s3.CreateBucketConfiguration{
			LocationConstraint: aws.String(""),
		},
	})
	return err
}

func (st *SimpleS3Template) BucketExists(ctx context.Context, bucket string) (bool, error) {
	client, err := st.factory.GetS3RawClient()
	if err != nil {
		return false, err
	}
	if _, err := client.HeadBucket(&s3.HeadBucketInput{Bucket: aws.String(bucket)}); err == nil {
		return true, nil
	}
	if errCode, ok := err.(awserr.Error); ok {
		if errCode.Code() == s3.ErrCodeNoSuchBucket {
			return false, nil
		}
	}
	return false, err
}

type UploadManaerTemplate struct {
	*SimpleS3Template
}

var _ S3Template = (*UploadManaerTemplate)(nil)

func (st *UploadManaerTemplate) MultiPartUpload(ctx context.Context, bucket string, key string, body io.ReadCloser) error {
	client, err := st.factory.GetUploadManagerClient()
	if err != nil {
		return err
	}
	_, err = client.Upload(&s3manager.UploadInput{
		Bucket: aws.String(bucket),
		Key:    aws.String(key),
		Body:   body,
	})
	return err
}
